/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.swak.spi;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.swak.registry.URL;
import com.swak.utils.StringUtils;

/**
 * Code generator for Adaptive class
 */
public class AdaptiveClassCodeGenerator {

	private static final Logger logger = LoggerFactory.getLogger(AdaptiveClassCodeGenerator.class);

	private static final String CODE_PACKAGE = "package %s;\n";

	private static final String CODE_IMPORTS = "import %s;\n";

	private static final String CODE_CLASS_DECLARATION = "public class %s$Adaptive implements %s {\n";

	private static final String CODE_METHOD_DECLARATION = "public %s %s(%s) %s {\n%s}\n";

	private static final String CODE_METHOD_ARGUMENT = "%s arg%d";

	private static final String CODE_METHOD_THROWS = "throws %s";

	private static final String CODE_UNSUPPORTED = "throw new UnsupportedOperationException(\"The method %s of interface %s is not adaptive method!\");\n";

	private static final String CODE_URL_NULL_CHECK = "if (arg%d == null) throw new IllegalArgumentException(\"url == null\");\n%s url = arg%d;\n";

	private static final String CODE_EXT_NAME_ASSIGNMENT = "String extName = %s;\n";

	private static final String CODE_EXT_NAME_NULL_CHECK = "if(extName == null) "
			+ "throw new IllegalStateException(\"Failed to get extension (%s) name from url (\" + url.toString() + \") use keys(%s)\");\n";

	private static final String CODE_EXTENSION_ASSIGNMENT = "%s extension = (%<s)%s.getExtensionLoader(%s.class).load(extName, arg0.getClass().getClassLoader());\n";

	private static final String CODE_EXTENSION_METHOD_INVOKE_ARGUMENT = "arg%d";

	private final Class<?> type;

	private String defaultExtName;

	public AdaptiveClassCodeGenerator(Class<?> type, String defaultExtName) {
		this.type = type;
		this.defaultExtName = defaultExtName;
	}

	/**
	 * test if given type has at least one method annotated with
	 * <code>Adaptive</code>
	 */
	private boolean hasAdaptiveMethod() {
		return Arrays.stream(type.getMethods()).anyMatch(m -> m.isAnnotationPresent(Adaptive.class));
	}

	/**
	 * generate and return class code
	 */
	public String generate() {
		// no need to generate adaptive class since there's no adaptive method found.
		if (!hasAdaptiveMethod()) {
			throw new IllegalStateException("No adaptive method exist on extension " + type.getName()
					+ ", refuse to create the adaptive class!");
		}

		StringBuilder code = new StringBuilder();
		code.append(generatePackageInfo());
		code.append(generateImports());
		code.append(generateClassDeclaration());

		Method[] methods = type.getMethods();
		for (Method method : methods) {
			code.append(generateMethod(method));
		}
		code.append("}");

		if (logger.isDebugEnabled()) {
			logger.debug(code.toString());
		}
		return code.toString();
	}

	/**
	 * generate package info
	 */
	private String generatePackageInfo() {
		return String.format(CODE_PACKAGE, type.getPackage().getName());
	}

	/**
	 * generate imports
	 */
	private String generateImports() {
		return String.format(CODE_IMPORTS, ExtensionLoader.class.getName());
	}

	/**
	 * generate class declaration
	 */
	private String generateClassDeclaration() {
		return String.format(CODE_CLASS_DECLARATION, type.getSimpleName(), type.getCanonicalName());
	}

	/**
	 * generate method not annotated with Adaptive with throwing unsupported
	 * exception
	 */
	private String generateUnsupported(Method method) {
		return String.format(CODE_UNSUPPORTED, method, type.getName());
	}

	/**
	 * get index of parameter with type URL
	 */
	private int getUrlTypeIndex(Method method) {
		int urlTypeIndex = -1;
		Class<?>[] pts = method.getParameterTypes();
		for (int i = 0; i < pts.length; ++i) {
			if (pts[i].equals(URL.class)) {
				urlTypeIndex = i;
				break;
			}
		}
		return urlTypeIndex;
	}

	/**
	 * generate method declaration
	 */
	private String generateMethod(Method method) {
		String methodReturnType = method.getReturnType().getCanonicalName();
		String methodName = method.getName();
		String methodContent = generateMethodContent(method);
		String methodArgs = generateMethodArguments(method);
		String methodThrows = generateMethodThrows(method);
		return String.format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows,
				methodContent);
	}

	/**
	 * generate method arguments
	 */
	private String generateMethodArguments(Method method) {
		Class<?>[] pts = method.getParameterTypes();
		return IntStream.range(0, pts.length)
				.mapToObj(i -> String.format(CODE_METHOD_ARGUMENT, pts[i].getCanonicalName(), i))
				.collect(Collectors.joining(", "));
	}

	/**
	 * generate method throws
	 */
	private String generateMethodThrows(Method method) {
		Class<?>[] ets = method.getExceptionTypes();
		if (ets.length > 0) {
			String list = Arrays.stream(ets).map(Class::getCanonicalName).collect(Collectors.joining(", "));
			return String.format(CODE_METHOD_THROWS, list);
		} else {
			return "";
		}
	}

	/**
	 * generate method URL argument null check
	 */
	private String generateUrlNullCheck(int index) {
		return String.format(CODE_URL_NULL_CHECK, index, URL.class.getName(), index);
	}

	/**
	 * generate method content
	 */
	private String generateMethodContent(Method method) {
		Adaptive adaptiveAnnotation = method.getAnnotation(Adaptive.class);
		StringBuilder code = new StringBuilder(512);
		if (adaptiveAnnotation == null) {
			return generateUnsupported(method);
		} else {
			int urlTypeIndex = getUrlTypeIndex(method);

			// found parameter in URL type
			if (urlTypeIndex != -1) {
				// Null Point check
				code.append(generateUrlNullCheck(urlTypeIndex));
			} else {
				// did not find parameter in URL type
				code.append(generateUrlAssignmentIndirectly(method));
			}

			String[] value = getMethodAdaptiveValue(adaptiveAnnotation);

			code.append(generateExtNameAssignment(value));
			// check extName == null?
			code.append(generateExtNameNullCheck(value));

			code.append(generateExtensionAssignment());

			// return statement
			code.append(generateReturnAndInvocation(method));
		}

		return code.toString();
	}

	/**
	 * generate code for variable extName null check
	 */
	private String generateExtNameNullCheck(String[] value) {
		return String.format(CODE_EXT_NAME_NULL_CHECK, type.getName(), Arrays.toString(value));
	}

	/**
	 * generate extName assigment code
	 */
	private String generateExtNameAssignment(String[] value) {
		// TODO: refactor it
		String getNameCode = null;
		for (int i = value.length - 1; i >= 0; --i) {
			if (i == value.length - 1) {
				if (null != defaultExtName) {
					if (!"protocol".equals(value[i])) {
						getNameCode = String.format("url.getParameter(\"%s\", \"%s\")", value[i], defaultExtName);
					} else {
						getNameCode = String.format("( url.getProtocol() == null ? \"%s\" : url.getProtocol() )",
								defaultExtName);
					}
				} else {
					if (!"protocol".equals(value[i])) {
						getNameCode = String.format("url.getParameter(\"%s\")", value[i]);
					} else {
						getNameCode = "url.getProtocol()";
					}
				}
			} else {
				if (!"protocol".equals(value[i])) {
					getNameCode = String.format("url.getParameter(\"%s\", %s)", value[i], getNameCode);
				} else {
					getNameCode = String.format("url.getProtocol() == null ? (%s) : url.getProtocol()", getNameCode);
				}
			}
		}

		return String.format(CODE_EXT_NAME_ASSIGNMENT, getNameCode);
	}

	/**
	 * @return
	 */
	private String generateExtensionAssignment() {
		return String.format(CODE_EXTENSION_ASSIGNMENT, type.getName(), ExtensionLoader.class.getSimpleName(),
				type.getName());
	}

	/**
	 * generate method invocation statement and return it if necessary
	 */
	private String generateReturnAndInvocation(Method method) {
		String returnStatement = method.getReturnType().equals(void.class) ? "" : "return ";

		String args = IntStream.range(0, method.getParameters().length)
				.mapToObj(i -> String.format(CODE_EXTENSION_METHOD_INVOKE_ARGUMENT, i))
				.collect(Collectors.joining(", "));

		return returnStatement + String.format("extension.%s(%s);\n", method.getName(), args);
	}

	/**
	 * get value of adaptive annotation or if empty return splitted simple name
	 */
	private String[] getMethodAdaptiveValue(Adaptive adaptiveAnnotation) {
		String[] value = adaptiveAnnotation.value();
		// value is not set, use the value generated from class name as the key
		if (value.length == 0) {
			String splitName = StringUtils.camelToSplitName(type.getSimpleName(), ".");
			value = new String[] { splitName };
		}
		return value;
	}

	/**
	 * get parameter with type <code>URL</code> from method parameter:
	 * <p>
	 * test if parameter has method which returns type <code>URL</code>
	 * <p>
	 * if not found, throws IllegalStateException
	 */
	private String generateUrlAssignmentIndirectly(Method method) {
		Class<?>[] pts = method.getParameterTypes();

		Map<String, Integer> getterReturnUrl = new HashMap<>();
		// find URL getter method
		for (int i = 0; i < pts.length; ++i) {
			for (Method m : pts[i].getMethods()) {
				String name = m.getName();
				if ((name.startsWith("get") || name.length() > 3) && Modifier.isPublic(m.getModifiers())
						&& !Modifier.isStatic(m.getModifiers()) && m.getParameterTypes().length == 0
						&& m.getReturnType() == URL.class) {
					getterReturnUrl.put(name, i);
				}
			}
		}

		if (getterReturnUrl.size() <= 0) {
			// getter method not found, throw
			throw new IllegalStateException("Failed to create adaptive class for interface " + type.getName()
					+ ": not found url parameter or url attribute in parameters of method " + method.getName());
		}

		Integer index = getterReturnUrl.get("getUrl");
		if (index != null) {
			return generateGetUrlNullCheck(index, pts[index], "getUrl");
		} else {
			Map.Entry<String, Integer> entry = getterReturnUrl.entrySet().iterator().next();
			return generateGetUrlNullCheck(entry.getValue(), pts[entry.getValue()], entry.getKey());
		}
	}

	/**
	 * 1, test if argi is null 2, test if argi.getXX() returns null 3, assign url
	 * with argi.getXX()
	 */
	private String generateGetUrlNullCheck(int index, Class<?> type, String method) {
		// Null point check
		StringBuilder code = new StringBuilder();
		code.append(String.format("if (arg%d == null) throw new IllegalArgumentException(\"%s argument == null\");\n",
				index, type.getName()));
		code.append(String.format(
				"if (arg%d.%s() == null) throw new IllegalArgumentException(\"%s argument %s() == null\");\n", index,
				method, type.getName(), method));

		code.append(String.format("%s url = arg%d.%s();\n", URL.class.getName(), index, method));
		return code.toString();
	}
}
